#!/usr/bin/env python
# encoding: utf-8

"""
File based session storage class.
Stores the data in a file on the harddisk, the cookie will always contain
the session ID needed to retrieve that file.
Supporst lazy initialization: the session data is not loaded in the __init__
method but only when (and if) you request some operation on that data.
"""

import hashlib
import time
import random
import os
from datetime import datetime, timedelta
import logging
import fcntl
try:
    import cPickle as pickle
except ImportError:
    import pickle

from spyro.internal.utils.io import safe_open
from spyro.session.storage.base import BaseStorage
from spyro.config.register import Register
from spyro.internal.utils.exceptions import SessionException

log = logging.getLogger(__name__)

class Storage(BaseStorage):
    """This class saves the session data on a file.
    """

    def __init__(self, request, response):
        """Tries to gain the session ID by the cookie as named in the configuration
        file, if it does not succeed will create a new session with a new
        session file.

        The session file will be created as "sess_`ID`" where `ID` is the
        session identifier.

        XXX: actually a wrong session identifier (unvalid) will raise a 500.
            How to handle it?
        """

        self.request = request
        self.response = response
        self._data = None # do not change this or it won't work

        cookie_name = Register.sessions['cookie']
        self._sessid = self.request.cookie(cookie_name, None)
        
        # if the session file doesn't exist restart a new session
        if self._sessid is not None:
            if not os.path.isfile(self.__get_filename()):
                self._sessid = None
        
        # now we do all this checks here penalizing only this
        # case with the appropriate investigations.
        if self._sessid is None:
            self._sessid = self.__generate_unique_id()
            self.response.set_cookie(cookie_name, self._sessid, httponly=True)

            # if the path does not exist, let's create it.
            # XXX: should this be done by hand?
            if not os.path.isdir(Register.sessions['folder']):
                os.mkdir(Register.sessions['folder'])

            # since the session identifier must be unique, the only way we have to
            # ensure it, is to create the session file right here.
            # just if the session is new
            self.__store_session_file()

        if self.__session_expired():
            self.destroy()
            self.__init__(self.request, self.response) # restart immediatly

        log.debug("Session ID: %s." % self._sessid)

    def __generate_unique_id(self):
        """Generates an unique session id by combining some factors.
        """

        sessid = None
        session_path = Register.sessions['folder']

        while True:
            # generating the session id
            # exit from this loop only if the session identifier is unique.

            x = Register.sessions['secret']
            y = str(time.time())
            z = str(random.random())

            sessid = hashlib.sha1((x + y + z).encode()).hexdigest()
            session_file_name = os.path.join(session_path, sessid)

            # if there is no file with this id, then it's unique.
            if not os.path.isfile(session_file_name):
                break

        return sessid

    def __get_filename(self):
        """Returns the session filename.
        """

        return os.path.join(Register.sessions['folder'],'sess_' + self._sessid)

    def __session_expired(self):
        """Returns True if the current session is expired.
        If not it returns False.
        """

        expiration = int(Register.sessions['expiration'])
        ctime = datetime.fromtimestamp(os.path.getctime(self.__get_filename()))
        expiry_time = ctime + timedelta(seconds=expiration)
        now = datetime.fromtimestamp(time.time())

        if now > expiry_time:
            return True
        else:
            return False

    def __load_session_file(self):
        """Loads the session file, and restores the session data.
        Locks the file during the manipulation to avoid race conditions in
        multiprocess environments. (thanks to Roberto De Ioris).
        """

        with safe_open(self.__get_filename(), 'rb') as (fn, err):
            if err:
                log.error('cannot load the session file %r' %
                            self.__get_filename())
            else:
                fcntl.flock(fn, fcntl.LOCK_EX)  # lock the file

                self._data = pickle.load(fn)

    def __store_session_file(self):
        """Stores the session data into the file.
        Locks the file during the manipulation to avoid race conditions in
        multiprocess environments. (thanks to Roberto De Ioris).
        """

        # we must store a dict not a NoneType
        # otherwise the __load_session_file will load a NoneType
        if self._data is None:
            self._data = dict()

        with safe_open(self.__get_filename(), 'wb') as (fn, err):
            if err:
                log.error('cannot store in the session file %r' %
                            self.__get_filename())
            else:
                fcntl.flock(fn, fcntl.LOCK_EX)  # lock the file
                pickle.dump(self._data, fn)

    def destroy(self):
        """It destroys the current session file and cleans the current
        session data.
        If the session has not been inited it results a non-operating function.
        """

        filename = self.__get_filename()
        
        os.remove(filename)

        self._data = None

        cookie_name = Register.sessions['cookie']
        self.response.delete_cookie(cookie_name)

    def __setitem__(self, key, value):
        """Stores the pair `key':`value'.
        """

        if self._data is None:
            self.__load_session_file()

        self._data[key] = value

        # auto-stores the changes
        self.__store_session_file()

    def __getitem__(self, key):
        """Get the "key" value.
        """

        if self._data is None:
            self.__load_session_file()

        try:
            return self._data[key]
        except KeyError:
            raise SessionException("Unsetted key: %r" % key)

    def __delitem__(self, key):
        """Deletes the "key" from the current session data.
        """

        if self._data is None:
            self.__load_session_file()

        try:
            del self._data[key]
        except KeyError:
            raise SessionException("Deleting an unsetted key: %r" % key)

        # auto-stores the changes
        self.__store_session_file()
    
    def __contains__(self, key):
        """Implement the support for the "in" operator on the session class.
        That's the prefered way to know if a key was set.
        """
        
        if self._data is None:
            self.__load_session_file()
        return key in self._data
